import java.rmi.*;
import java.rmi.registry.*;
import java.rmi.server.*;
import java.util.*;
import java.io.*;
import java.net.*;

public class server extends UnicastRemoteObject {
	
	// IP of the service broker
	private String broker;
	
	// Port to publish RMI services on
	private int port; 
	
	// IP address of this service provider
	String IP;
	
	// RMI registry object which we will publish services to
	private Registry reg;
	
    // Socket to broker
    private Socket conn;
    
    // Keep track of services registered so that they can be removed from
    // the Broker on shutdown
    private Map<String, String> services;
    
    // Maping of known error codes to descriptions
    private HashMap<String, String> errors;

    // Input and Output streams (to/from broker)
    private BufferedReader in;
    private BufferedWriter out;
	
	/* 
	* Starts a new Service Provider server. This invloves handshaking with the
	* Broker and setting up the RMI registry 
	*/
	public server(String ip, int port) throws RemoteException {
		super();
        this.errors = new HashMap<String,String>();
        this.services = new HashMap<String,String>();
		this.port = port;
		this.broker = ip;
		Runtime.getRuntime().addShutdownHook(new Shutdown());
        errors.put("0x001", "ERROR 0x001: Port already in use - server cannot start");
        errors.put("0x002", "ERROR 0x002: Protocol sequence error");
        errors.put("0x003", "ERROR 0x003: Incorrect or malformed protocol message");
        errors.put("0x004", "ERROR 0x004: Socket, I/O exception caught");
        errors.put("0x005", "ERROR 0x005: Unknown protocol message received at server");
		reg = LocateRegistry.createRegistry(1337);
		connect(ip, port);
		
	}

	public void connect(String ip, int port) {
		try {
			System.out.printf("Connecting to %s:%d%n", ip, port);
			this.conn = new Socket(ip, port);
            this.in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            this.out = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream()));
            send("HELLO");
            while(true) {
				IP = InetAddress.getLocalHost().getHostAddress();
                String message = get(in);
                if(message.toLowerCase().equals("hi")) 
                    System.out.println("Received HI message from server, we're good to go");
                else {
                    System.err.println("Received impropper message " + message + " from server");
                }
                break;
            }
		} catch (java.net.UnknownHostException e) {
			System.err.println(String.format("Unknow host at %s:%d", ip, port));
		} catch (IOException e) {
			System.err.println(String.format("Could not connect to broker: %s%n", e.getMessage()));
		}
	}

	/*
	* Returns a line of text from input stream. Blocks until one is available 
	*/
	public String get(BufferedReader in) throws IOException {
		int c;
		char nl = '\n';
		StringBuilder sb = new StringBuilder();
		while((c = in.read()) != nl && c != -1) 
			sb.append((char)c);
		return sb.toString();
	}

	/*
	* Sends message plus newline to output stream 
	*/
	public void send(String message) throws IOException {
		System.err.println("Sending " + message + " message");
		out.write(message);
		out.newLine();
		out.flush();
    }

    /*
    * Shutdown client. Sends BYE message to server and closes all
    * sockets and streams 
    */
	public void shutdown() {
		try {
			send("BYE");
			in.close();
			out.close();
			conn.close();
		} catch (IOException e) {
			System.err.println(e);
		}
	}
	
	/* 
	* Registers a new RMI service with the broker. If there is already a service
	* with a lower version number registered from this service provider, it removes
	* it from the broker and adds the newer one.
	*/
	public void register(String service, String version) throws IOException {
		if(services.containsKey(service) && 
		   services.get(service).compareTo(version) < 0 ) {
			remove(service, version);
		}	
		send(String.format("REGISTER %s %s %s", service, IP, version));
		String[] response = get(in).split(" ");
		System.out.println(Arrays.toString(response));
		if(response[0].equals("ERROR")) 
			System.err.println(errors.get(response[1]));
		else {
			System.out.printf("REGISTER (%s) %s\n", service,
				response[1].equals("true") ? "successful" : "unsuccessful");
			services.put(service,version);
		}
	}
	
	/* 
	* Removes a service from the Broker database. 
	*/
	public void remove(String service, String version) throws IOException {
		send(String.format("REMOVE %s %s %s", service, IP, version));
		String[] response = get(in).split(" ");
		System.out.println(Arrays.toString(response));
		if(response[0].equals("ERROR"))
			System.err.println(errors.get(response[1]));
		else 
			System.out.printf("REMOVE (%s) %s\n", service,
				response[1].equals("true") ? "successful" : "unsuccessful");
	}
	
	/*
	* Handles a request by creating and publishing the RMI service specified.
	* Also registers the service with the broker.
	*/
	public boolean handle(String command, boolean file) {
		if(command.equals("exit") && !file) return false;
		String[] token = command.split(" +");
		if(token[0].equals("register")) {
			try {
				String[] service = token[1].split("_");
				Remote sp;
				String bindname;
				// generate_random is the one service with a name that must be handled separately
				if(service[0].equals("generate")) {
					sp = new generate_random_provider();
					bindname = "random";
				} else {
				// services are called service_suffix. Interfaces are called service_provider
				// Reflection is used to dynamically instanciate the proper class 
					Class serviceClass = Class.forName(token[1] + "_provider");
					sp = (Remote)serviceClass.newInstance();
					bindname = service[0];
				}
				// publish RMI service and register with the broker 
				reg.rebind(bindname, sp);
				register(bindname, token[2]);
				System.out.println("Currently registered services" + Arrays.toString(reg.list()));
			} catch (ClassNotFoundException e) {
				System.err.println("Service does not exist: " + e.getMessage());
			} catch (Exception e) {
				System.err.println(e.getMessage());
				try {
					System.out.printf("Trying to reconnect to %s:%d%n", IP, port);
					connect(IP, port);
		        } catch (Exception ex) {
		        	System.out.printf("Could not reconnect to broker at %s. Exiting%n", IP);
		        	return false;
		        }
			}
		} else if(token[0].equals("remove")) {
			String bindname = "";
			String version = "";
			try {
				bindname = token[1];
				version = token[2];
				reg.unbind(bindname);
				remove(bindname, version);
			} catch (NotBoundException e) {
				System.out.printf("Service '%s' not bound%n", bindname);
			} catch (Exception e) {
				System.out.println(e.getMessage());
				try {
					System.out.printf("Trying to reconnect to %s:%d%n", IP, port);
					connect(IP, port);
		        } catch (Exception ex) {
		        	System.out.printf("Could not reconnect to broker at %s. Exiting%n", IP);
		        	return false;
		        }	
			}
		}
		return true;
	}
	
	public static void main(String[] arg) throws RemoteException {
		if(arg.length > 3 || arg.length < 2) {
			System.err.println("Usage: server <broker IP> <broker port> [script file]");
			System.exit(0);
		}
		
		boolean interactive = false;
		String ip = "";
		int port = 0;
		if(arg.length == 2) interactive = true;

		ip = arg[0];
		try { 
			port = Integer.parseInt(arg[1]);
		} catch (NumberFormatException e) {
			System.err.println("broker port must be an integer");
			System.exit(0);
		}
		
		Scanner in;
		if(interactive) 
			in = new Scanner(System.in);
		else {
			try { 
				in = new Scanner(new File(arg[2]));
			} catch (FileNotFoundException e) {
				System.out.println("Script file not found. Defaulting to interactive client");
				in = new Scanner(System.in);
			}
		}

		server svr = new server(ip, port);
		boolean running = true;
		
		// if a file was specified, run all commands in it first
		if(!interactive) {
			while(in.hasNext() && running) 
				running = svr.handle(in.nextLine().toLowerCase(), true);
			in = new Scanner(System.in);
		}

		// handle commands from the command line
		do {
			running = svr.handle(in.nextLine().toLowerCase(), false);
		} while(running && in.hasNext());

		System.exit(0);
	}
	
	private class Shutdown extends Thread {
		public void run() {
			System.out.println("Server shutting down");
			for(String service : services.keySet()) {
				try {
					remove(service, services.get(service));
				} catch (Exception e) {
					System.err.println("Problem removing service from broker: " + e.getMessage());
				}
			}
			shutdown();
		}
	}
}
